package org.gap.jseed.injection;

import static org.gap.jseed.utils.StringUtils.indentAllLines;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;

import org.gap.jseed.utils.JSeedDefect;

public class MethodWriter {
	private final Method method;
	private List<String> insertBeforeCode;
	private String body;
	private List<String> insertAfterCode;
	private boolean ensureReturn = true;
	
	public MethodWriter(Method method) {
		this.method = method;
		insertBeforeCode = new LinkedList<String>();
		insertAfterCode = new LinkedList<String>();
	}

	public void insertBefore(String codeBlock) {
		insertBeforeCode.add(codeBlock);
	}
	
	public void setBody(String source) {
		if (body == null) {
			this.body = source;
		} else {
			throw new JSeedDefect("Method body has already been set for [" + method.getName() + "] with [" + body + "]," +
					"\n  Cannot set new source to [" + source + "].");
		}
	}
	
	public void ensureReturn(boolean state) {
		this.ensureReturn = state;
	}
	
	public void insertAfter(String code) {
		insertAfterCode.add(code);
	}

	public void writeMethod(CtClass toModify) {
		if (hasNoCodeToInject()) {
			return;
		}
		String createMethodSourceCode = createMethodSourceCode();
		try {
			CtMethodLocator locator = new CtMethodLocator(toModify);
			CtMethod ctMethod = locator.find(method);
			CtMethod newMethod = new CtMethod(ctMethod, toModify, null);
			newMethod.setBody(createMethodSourceCode);
			toModify.addMethod(newMethod);
		} catch (CannotCompileException e) {
			String message = String.format("%s in %s.%s(..):\n\n%s", e.getMessage(), toModify.getName(), method.getName(), indentAllLines(createMethodSourceCode));
			throw new InvalidCodeFormatException(message);
		}
	}

	private boolean hasNoCodeToInject() {
		return insertBeforeCode.isEmpty() && body == null  && insertAfterCode.isEmpty();
	}

	private String createMethodSourceCode() {
		String result = "";
		for (String each : insertBeforeCode) {
			result += each + ";\n";
		}
		if (body != null) {
			result += body + ";\n";
		}
		
		String callSuperMethod = "super." + method.getName() + "($$);\n";
		if (!insertAfterCode.isEmpty()) {
			if (!isVoidReturn()) {
				result += method.getReturnType().getName() + " result = " + callSuperMethod;
			} else {
				result += callSuperMethod;
			}
			for (String each : insertAfterCode) {
				result += each + ";\n";
			}
			if (!isVoidReturn()) {
				result += "\nreturn result;\n";
			} else {
				result += ";\n";
			}
		}
		if (!Modifier.isAbstract(method.getModifiers()) && ensureReturn) {
			if (isVoidReturn()) {
				if (!(result.contains(callSuperMethod))) {
					result += callSuperMethod;
				} 
			} else if (!result.contains("return")) {
				result += "return " + callSuperMethod;
			}
		}
		return "{\n" + result + "\n}";
	}

	private boolean isVoidReturn() {
		return method.getReturnType().getName().equals("void");
	}
}